﻿using System;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
using System.Xml.XPath;

namespace InfoPathHelpers.Cryptography.Authentication
{
    /// <summary>
    /// Used to provide authentication for PC's which cannot use windows authentication but require some
    /// security to prevent unauthorised access.
    /// </summary>
    public class KeyFile
    {
        private string _FileName;

        private const string USER_NAME_XPATH = "/KeyFile/UserName";
        private const string ROLE_XPATH = "/KeyFile/Role";
        private const string WINDOWS_ID_XPATH = "/KeyFile/WindowsId";
        private const string PASSWORD_XPATH = "/KeyFile/Password";
        private const string CERTIFICATE_SUBJECT_XPATH = "/KeyFile/CertificateSubject";
        private const string AUTHORISATION_KEY_XPATH = "/KeyFile/AuthorisationKey";
        private const string AUTHORISATION_DATE_XPATH = "/KeyFile/AuthorisationDate";

        /// <summary>
        /// Creates a new KeyFile object using the fileLocation of the supplied dataSourceContext.
        /// If the underlying KeyFile does not exist it will be create on Save.
        /// </summary>
        /// <param name="dataSourceContext">The FileDataSourceContext referencing the KeyFile.</param>
        public KeyFile(FileDataSourceContext dataSourceContext) : this(dataSourceContext.FileLocation) { }

        /// <summary>
        /// Creates a new KeyFile object using the fileName supplied.
        /// /// If the underlying KeyFile does not exist it will be create on Save.
        /// </summary>
        /// <param name="fileName">The path to the KeyFile.</param>
        public KeyFile(string fileName)
        {
            _FileName = fileName;
            Password = string.Empty;
            if (Exists)
            {
                var KeyFileDocument = new XmlDocument();
                KeyFileDocument.Load(_FileName);
                InitializeData(KeyFileDocument.CreateNavigator());
            }
        }

        private void InitializeData(XPathNavigator keyFileNavigator)
        {
            UserName = NodeOperations.GetValue<string>(keyFileNavigator.SelectSingleNode(USER_NAME_XPATH));
            Role = NodeOperations.GetValue<string>(keyFileNavigator.SelectSingleNode(ROLE_XPATH));
            WindowsId = NodeOperations.GetValue<string>(keyFileNavigator.SelectSingleNode(WINDOWS_ID_XPATH)); 
            CertificateSubject = NodeOperations.GetValue<string>(keyFileNavigator.SelectSingleNode(CERTIFICATE_SUBJECT_XPATH));
            Password = string.Empty;
            AuthorisationKey = NodeOperations.GetValue<string>(keyFileNavigator.SelectSingleNode(AUTHORISATION_KEY_XPATH));
            AuthorisationDate = NodeOperations.GetValue<DateTime>(keyFileNavigator.SelectSingleNode(AUTHORISATION_DATE_XPATH));
        }

        /// <summary>
        /// The display name of the user being authorised.
        /// </summary>
        public string UserName { get; set; }

        /// <summary>
        /// The role for which the user is authorised.
        /// </summary>
        public string Role { get; set; }

        /// <summary>
        /// The WindowsId of the user being authorised (used to restrict access to other users).
        /// </summary>
        public string WindowsId { get; set; }

        /// <summary>
        /// Gets or sets the password for the key file (used to provide additional protection if required).
        /// </summary>
        public string Password { get; set; }

        /// <summary>
        /// The fully qualifed subject name from the certificate (used to locate the certificate for validation).
        /// </summary>
        public string CertificateSubject { get; set; }

        /// <summary>
        /// The authorisation key created at the time of request
        /// (used to check the machine is the one from which the request originated).
        /// </summary>
        public string AuthorisationKey { get; set; }

        /// <summary>
        /// The date and time on which the KeyFile was created or signed.
        /// </summary>
        public DateTime AuthorisationDate { get; set; }

        /// <summary>
        /// Gets a boolean value indicating whether or not the underlying file exists.
        /// </summary>
        public bool Exists
        {
            get { return File.Exists(_FileName); }
        }

        /// <summary>
        /// Deletes the underlying file.
        /// </summary>
        public void Delete()
        {
            if(File.Exists(_FileName))
            {
                File.Delete(_FileName);
            }
        }

        /// <summary>
        /// Saves they KeyFile to the path from which it was opened.
        /// </summary>
        public void Save()
        {
            this.Save(_FileName);
        }

        /// <summary>
        /// Saves the keyfile to the specified path. Existing files are overwritten.
        /// </summary>
        /// <param name="fileName"></param>
        public void Save(string fileName)
        {
            // Create a blank key file.
            _FileName = fileName;
            CreateBlankKeyFile(_FileName);

            // Load the key file.
            XmlDocument KeyFile = new XmlDocument();
            KeyFile.Load(_FileName);

            // Set the values of all fields.
            XPathNavigator KeyFileNavigator = KeyFile.CreateNavigator();
            //InitializeData(KeyFileNavigator);

            KeyFileNavigator.SelectSingleNode(USER_NAME_XPATH).SetValue(UserName);
            KeyFileNavigator.SelectSingleNode(ROLE_XPATH).SetValue(Role);
            KeyFileNavigator.SelectSingleNode(WINDOWS_ID_XPATH).SetValue(WindowsId);
            KeyFileNavigator.SelectSingleNode(PASSWORD_XPATH).SetValue(Password);
            KeyFileNavigator.SelectSingleNode(CERTIFICATE_SUBJECT_XPATH).SetValue(CertificateSubject);
            KeyFileNavigator.SelectSingleNode(AUTHORISATION_KEY_XPATH).SetValue(AuthorisationKey);
            KeyFileNavigator.SelectSingleNode(AUTHORISATION_DATE_XPATH).SetValue(AuthorisationDate.ToString("s", CultureInfo.CurrentCulture));

            // Save to the requested filepath.
            KeyFile.Save(_FileName);
        }

        public bool IsSignatureValid(X509Certificate2 certificate)
        {
            if(certificate == null)
            {
                throw new ArgumentNullException("certificate");
            }

            RSACryptoServiceProvider CryptoServiceProvider = RSAHelper.GetRSACryptoProvider(certificate, RSAKeyType.Public);
            return XmlSignaturesHelper.ValidateSignature(_FileName, CryptoServiceProvider);
        }

        public bool IsCurrentMachineAuthorised()
        {
            Authenticator Authenticator = new Authenticator();
            return Authenticator.AuthenticateKey(AuthorisationKey);
        }

        /// <summary>
        /// Gets the certificate from the personal store which has a 
        /// matching SubjectName to that with which the KeyFile was signed.
        /// </summary>
        /// <returns>An X509Certificate2 object referencing the certificate 
        /// used to sign this KeyFile or null if it could not be located.</returns>
        public X509Certificate2 GetCertificate()
        {
            return X509Helper.GetCertificateBySubject(CertificateSubject);
        }

        /// <summary>
        /// Confirms that both the KeyFile has a valid signature and the current machine is
        /// the same as the one for which the KeyFile was authorised.
        /// </summary>
        /// <param name="certificate">The certificate which was used to sign the KeyFile.</param>
        /// <returns></returns>
        public bool IsAuthentic(X509Certificate2 certificate)
        {
            return Exists && IsSignatureValid(certificate) && IsCurrentMachineAuthorised();
        }

        /// <summary>
        /// Saves the KeyFile then signs it with the supplied certificate.
        /// </summary>
        /// <param name="fileName">The path to save the signed KeyFile to.</param>
        public void Authorise(string fileName, X509Certificate2 certificate)
        {
            _FileName = fileName;
            if(certificate == null)
            {
                throw new ArgumentNullException("certificate");
            }

            if(!certificate.HasPrivateKey)
            {
                throw new ArgumentException("The certificate must have a valid private key.", "certificate");
            }

            AuthorisationDate = DateTime.Now;
            CertificateSubject = certificate.Subject;
            Save(_FileName);
            XmlSignaturesHelper.SignDocument(_FileName, _FileName,
                    RSAHelper.GetRSACryptoProvider(certificate, RSAKeyType.Private));
        }

        /// <summary>
        /// Creates a blank KeyFile using the specified filename.
        /// </summary>
        /// <param name="filename">The path at which to create the file.</param>
        public static void CreateBlankKeyFile(string filename)
        {
            XmlWriterSettings WriterSettings = new XmlWriterSettings();
            WriterSettings.Indent = true;
            WriterSettings.IndentChars = "\t";

            using (XmlWriter Writer = XmlWriter.Create(filename, WriterSettings))
            {
                Writer.WriteStartDocument();
                Writer.WriteStartElement("KeyFile");
                Writer.WriteElementString("UserName", string.Empty);
                Writer.WriteElementString("Role", string.Empty);
                Writer.WriteElementString("WindowsId", string.Empty);
                Writer.WriteElementString("Password", string.Empty);
                Writer.WriteElementString("CertificateSubject", string.Empty);
                Writer.WriteElementString("AuthorisationKey", string.Empty);
                Writer.WriteElementString("AuthorisationDate", DateTime.Now.ToString("s", CultureInfo.CurrentCulture));
                Writer.WriteEndDocument();
            }
        }
    }
}
